import math
import requests
from binance.um_futures import UMFutures
import time
import logging
import pandas as pd
import openpyxl


# 通过网页登录到币安网页版 进入跟单详情页面 https://www.binance.com/zh-CN/copy-trading
# 跟单带单接口都是非官方公开API F12之后复制一个xhr请求 复制为cURL(bash)
# 之后通过https://curlconverter.com/将cURL快速转换成requests得到cookies和headers 这个网站仅在本地做转换 可以放心使用
cookies = {}
headers = {}

# 带单者的id 点击带单者的详情页可以看到 例如ddbxxb的详情页 https://www.binance.com/zh-CN/copy-trading/lead-details/3962663535186762752?timeRange=90D
# followed_leader_id为 3962663535186762752 我的带单id同理
followed_leader_id = ''
my_leader_id = ''

# 同步间隔时长 例如每隔1s同步仓位 时间过长会导致滑点过大 过短请求过于频繁
sync_wait_time = 1

# binance API key 开启读取权限就行 主要用来读取开仓前的ticker价格 用于滑点判断
api_key = ""
api_secret = ""
um_futures_client = UMFutures(api_key, api_secret)

# 滑点控制 这个不太好把握 开启了之后可能开仓平仓不同步 可能出现开了仓没及时平掉
enable_slippage_protection = False
slippage = 0.1


# 日志级别配置
logging.basicConfig(
    filename='./binance_copy_trading_bot.log',
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logger = logging.getLogger(__name__)

session = requests.Session()
session.headers.update(headers)
session.cookies.update(cookies)


def fetch_portfolio(portfolio_id):
    params = {
        'portfolioId': portfolio_id,
    }
    url = 'https://www.binance.com/bapi/futures/v1/friendly/future/copy-trade/lead-data/positions'
    response = session.get(url, params=params).json()
    expected_status_code = '000000'
    assert response['code'] == expected_status_code, f"{response['message']}, {response['messageDetail']}"
    return response['data']


def fetch_userinfo():
    url = "https://www.binance.com/bapi/futures/v1/private/future/copy-trade/home-page/user-info"
    response = session.get(url).json()
    expected_status_code = '000000'
    assert response['code'] == expected_status_code, f"{response['message']}, {response['messageDetail']}"
    return response['data']


def fetch_leader_detail(portfolio_id):
    params = {
        'portfolioId': portfolio_id,
    }
    url = "https://www.binance.com/bapi/futures/v1/friendly/future/copy-trade/lead-portfolio/detail"
    response = session.get(url, params=params).json()
    expected_status_code = '000000'
    assert response['code'] == expected_status_code, f"{response['message']}, {response['messageDetail']}"
    return response['data']


def fetch_leverage(portfolio_id):
    query_body = {
      "copyTradeType": "PUB_LEAD",
      "portfolioId": portfolio_id
    }
    url = "https://www.binance.com/bapi/futures/v1/private/future/user-data/userLeverage"
    response = session.post(url, json=query_body).json()
    expected_status_code = '000000'
    assert response['code'] == expected_status_code, f"{response['message']}, {response['messageDetail']}"
    return response['data']


def is_same_leverage(leverage_dict, _symbol, expected_leverage):
    return len([item for item in leverage_dict if item.get('symbol') == _symbol and item.get('leverage') == expected_leverage]) > 0


def adjust_leverage(symbol, portfolio_id, leverage=5):
    leverage_info = {
      "symbol": symbol,
      "leverage": leverage,
      "copyTradeType": "PUB_LEAD",
      "portfolioId": portfolio_id
    }
    url = "https://www.binance.com/bapi/futures/v1/private/future/user-data/adjustLeverage"
    response = session.post(url, json=leverage_info).json()
    expected_status_code = '000000'
    assert response['code'] == expected_status_code, f"{response['message']}, {response['messageDetail']}"


def place_order(command, _symbol, position_side, quantity, price, portfolio_id, leverage, isolated=False, time_in_force="GTC", order_type="MARKET"):
    # 生效时间t ime_in_force
    # • GTC (生效直到取消): 此种订单将持续有效直到完全成交或被取消。
    # • IOC (立即成交或取消): 此种订单将会立即成交全部或部分订单，并且取消剩余未成交的部分。
    # • FOK (全部成交或取消): 此种订单必须立即全部成交，否则将被全部取消。

    if command is None:
        return None

    order_info = {}

    limit_order_open = {
        "symbol": _symbol,
        "type": "LIMIT",
        "side": "SELL" if position_side == "SHORT" else "BUY",
        "positionSide": position_side,
        "quantity": quantity,
        "timeInForce": time_in_force,
        "price": price,
        "copyTradeType": "PUB_LEAD",
        "portfolioId": portfolio_id,
        "placeType": "order-form"
    }

    limit_order_close = {
        "symbol": _symbol,
        "type": "LIMIT",
        "side": "BUY" if position_side == "SHORT" else "SELL",
        "quantity": quantity,
        "price": price,
        "positionSide": position_side,
        "leverage": leverage,
        "isolated": isolated,
        "timeInForce": time_in_force,
        "copyTradeType": "PUB_LEAD",
        "portfolioId": portfolio_id,
        "placeType": "position"
    }

    market_order_open = {
        "symbol": _symbol,
        "type": "MARKET",
        "side": "SELL" if position_side == "SHORT" else "BUY",
        "positionSide": position_side,
        "quantity": quantity,
        "copyTradeType": "PUB_LEAD",
        "portfolioId": portfolio_id,
        "placeType": "order-form"
    }

    market_order_close = {
      "symbol": _symbol,
      "type": "MARKET",
      "side": "BUY" if position_side == "SHORT" else "SELL",
      "quantity": quantity,
      "positionSide": position_side,
      "leverage": leverage,
      "isolated": isolated,
      "copyTradeType": "PUB_LEAD",
      "portfolioId": portfolio_id,
      "newOrderRespType": "RESULT",
      "placeType": "position"
    }

    if order_type == "LIMIT" and command == "OPEN":
        order_info = limit_order_open
    if order_type == "LIMIT" and command == "CLOSE":
        order_info = limit_order_close
    if order_type == "MARKET" and command == "OPEN":
        order_info = market_order_open
    if order_type == "MARKET" and command == "CLOSE":
        order_info = market_order_close

    # logger.info(order_info)
    url = "https://www.binance.com/bapi/futures/v1/private/future/order/place-order"
    response = session.post(url, json=order_info).json()
    expected_status_code = '000000'
    assert response['code'] == expected_status_code, f"{response['message']}, {response['messageDetail']}"
    return response['data']


def query_all_leader_list(page_size, total):
    total_info = query_leader_list(1, 1)
    total_size = min(total, total_info['total'])
    all_info_list = []
    for page in range(1, math.floor(total_size / page_size) + 1):
        one_page = query_leader_list(page, page_size)
        all_info_list.extend(one_page['list'])
    return all_info_list


def query_leader_list(page_num=1, page_size=20):
    query_info = {
      "pageNumber": page_num,
      "pageSize": page_size,
      "timeRange": "90D",
      "dataType": "AUM",
      "favoriteOnly": False,
      "hideFull": False,
      "nickname": "",
      "order": "DESC",
      "userAsset": 16907.06191725,
      "portfolioType": "PUBLIC"
    }
    url = "https://www.binance.com/bapi/futures/v1/friendly/future/copy-trade/home-page/query-list"
    response = session.post(url, json=query_info).json()
    expected_status_code = '000000'
    assert response['code'] == expected_status_code, f"{response['message']}, {response['messageDetail']}"
    return response['data']


def craw_leader_to_excel():
    leader_info_list = query_all_leader_list(20, 100)
    data_to_write = []
    for info in leader_info_list:
        data_to_write.append({
            'Nickname': info['nickname'],
            'Lead Portfolio ID': info['leadPortfolioId'],
            'AUM': info['aum'],
            'ROI': info['roi'],
            'PNL': info['pnl'],
            'MDD': info['mdd'],
            'Win Rate': info['winRate']
        })

    # 创建 DataFrame
    df = pd.DataFrame(data_to_write)
    excel_file_path = './portfolio_info.xlsx'
    df.to_excel(excel_file_path, index=True)


def print_leader_detail(leader_detail):
    logger.info(f"================{leader_detail['nickname']}===================")
    logger.info(f"带单保证金: {leader_detail['marginBalance']}")
    logger.info(f"资产管理规模: {leader_detail['aumAmount']}")
    logger.info(f"跟单者盈亏: {leader_detail['copierPnl']}")
    logger.info(
        f"最小带单金额: 定额 {leader_detail['fixedAmountMinCopyUsd']} | 定比 {leader_detail['fixedRadioMinCopyUsd']}")
    logger.info(f"分润比例: {leader_detail['profitSharingRate']}")
    logger.info("=========================================\n")


def print_leader_position(leader_position_list):
    logger.info(f"================持仓信息===================")
    for leader_position in leader_position_list:
        logger.info("---------------------------------")
        logger.info(f"{'空' if leader_position['positionSide'] == 'SHORT' else '多'} {leader_position['symbol']} {'逐仓' if leader_position['isolated'] else '全仓'}{leader_position['leverage']}倍")
        logger.info(f"未实现盈亏: {leader_position['unrealizedProfit']}{leader_position['collateral']} 持仓数量: {abs(float(leader_position['positionAmount']))}  保证金: {abs(float(leader_position['notionalValue']) / leader_position['leverage'])}")
        logger.info(f"开仓价: {leader_position['entryPrice']} 标记价格: {leader_position['markPrice']}")
        logger.info("---------------------------------")
    logger.info("=========================================\n")


my_leverage_dict = fetch_leverage(my_leader_id)


def sync_position():
    leader_detail = fetch_leader_detail(followed_leader_id)
    leader_margin_balance = float(leader_detail['marginBalance'])
    print_leader_detail(leader_detail)

    leader_portfolio = fetch_portfolio(followed_leader_id)
    leader_cur_open_position = [item for item in leader_portfolio if item.get('adl') != 0]
    print_leader_position(leader_cur_open_position)

    my_detail = fetch_leader_detail(my_leader_id)
    my_margin_balance = float(my_detail['marginBalance'])
    print_leader_detail(my_detail)

    my_portfolio = fetch_portfolio(my_leader_id)
    my_cur_open_position = [item for item in my_portfolio if item.get('adl') != 0]
    print_leader_position(my_cur_open_position)

    for leader_position in leader_cur_open_position:
        leader_symbol = leader_position['symbol']
        leader_position_side = leader_position['positionSide']
        leader_position_amount = abs(float(leader_position["positionAmount"]))
        leader_entry_price = leader_position['entryPrice']
        leader_leverage = leader_position['leverage']
        leader_isolated = leader_position['isolated']

        my_should_hold_position_amount = leader_position_amount * my_margin_balance / leader_margin_balance

        # 判断我是否有同品种同方向仓位
        my_same_side_symbol_position = [item for item in my_cur_open_position if
                                        item.get('symbol') == leader_symbol and item.get(
                                            'positionSide') == leader_position_side]

        if len(my_same_side_symbol_position) > 1:
            logger.error("存在多条同品种同方向仓位历史数据，请关注")
            continue

        # 杠杆对齐
        if not is_same_leverage(my_leverage_dict, leader_symbol, leader_leverage):
            logger.info(f"调整 {leader_symbol} 杠杆数量和带单员一致: {leader_leverage}倍")
            adjust_leverage(leader_symbol, my_leader_id, leader_leverage)

        command = None
        quantity = 0
        my_position_amount = 0

        if len(my_same_side_symbol_position) == 1:
            # 已有仓位，检查同方向同品种的持仓数据是否有变化
            my_position_info = my_same_side_symbol_position[0]
            my_position_amount = abs(float(my_position_info['positionAmount']))

            position_amount_diff = my_should_hold_position_amount - my_position_amount
            if position_amount_diff < 0:
                # 仓位过大 减仓
                command = "CLOSE"
                quantity = abs(position_amount_diff)
            elif position_amount_diff > 0:
                # 仓位过小 加仓
                command = "OPEN"
                quantity = position_amount_diff
        else:
            # 新增仓位
            command = "OPEN"
            quantity = my_should_hold_position_amount

        # 带单员新增的仓位 全仓
        cur_price = um_futures_client.ticker_price(leader_symbol)['price']
        if enable_slippage_protection:
            if leader_position_side == 'SHORT' and (float(leader_entry_price) - float(cur_price)) > slippage:
                continue
            if leader_position_side == 'LONG' and (float(cur_price) - float(leader_entry_price)) > slippage:
                continue
        if command is not None and quantity / my_should_hold_position_amount > 0.05:
            logger.info(f"{leader_symbol} 应该持仓: {my_should_hold_position_amount} 实际持仓: {my_position_amount} {'加仓' if command == 'OPEN' else '减仓'}: {quantity} 价格: {cur_price}")
            order_info = place_order(command, leader_symbol, leader_position_side, quantity, cur_price,
                                     my_leader_id, leader_leverage, leader_isolated)
            logger.info(f"{'开' if command == 'OPEN' else '平'}{'空' if order_info['positionSide'] == 'SHORT' else '多'} 成交价格: {order_info['price']} 成交数量: {order_info['origQty']}")

    for my_position in my_cur_open_position:
        my_symbol = my_position['symbol']
        my_position_side = my_position['positionSide']
        my_position_amount = abs(float(my_position['positionAmount']))
        my_leverage = my_position['leverage']
        my_isolated = my_position['isolated']
        my_same_side_symbol_position = [item for item in leader_cur_open_position if
                                        item.get('symbol') == my_symbol and item.get(
                                            'positionSide') == my_position_side]
        if len(my_same_side_symbol_position) != 0:
            continue
        # 我比带单员多出来的仓位 全部平仓
        cur_price = um_futures_client.ticker_price(my_symbol)['price']
        command = "CLOSE"
        logger.info(f"{my_symbol} 应该持仓: 0 实际持仓: {my_position_amount} {'加仓' if command == 'OPEN' else '减仓'}: {my_position_amount} 价格: {cur_price}")
        order_info = place_order(command, my_symbol, my_position_side, my_position_amount, cur_price,
                                 my_leader_id, my_leverage, my_isolated)
        logger.info(f"{'开' if command == 'OPEN' else '平'}{'空' if order_info['positionSide'] == 'SHORT' else '多'} 成交价格: {order_info['price']} 成交数量: {order_info['origQty']}")


if __name__ == "__main__":
    while True:
        sync_position()
        time.sleep(sync_wait_time)
